﻿using Microsoft.Office.Interop.Excel;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Xml;

namespace SIR.TestReportGenerator
{
	/// <summary>
	/// Generator for the Excel test document output
	/// </summary>
	class Program
	{
		static void Main(string[] args)
		{
			_directory = args[0] as string;
			// Find the VS test output files
			var vsFiles = System.IO.Directory.GetFiles(_directory, "*.trx", System.IO.SearchOption.TopDirectoryOnly);
			// Find the Chutzpah output files
			var jsFile = System.IO.Directory.GetFiles(_directory, "*.xml", System.IO.SearchOption.TopDirectoryOnly);
			// Process all of the VS files
			foreach (var file in vsFiles)
			{
				ProcessVSFile(file);
			}
			// Process the Chutzpah files
			foreach (var file in jsFile)
			{
				ProcessJUnitFile(file);
			}
			// Open an excel workbook
			var xlApp = new Microsoft.Office.Interop.Excel.Application();
			Workbook wb = xlApp.Workbooks.Add(XlWBATemplate.xlWBATWorksheet);
			Worksheet ws = null;
			// Group all of the areas
			var areas = from tc in Tests.Values
						group tc by tc.Area into g
						orderby g.Key
						select g;
			// Create a sheet for all of the tests
			var all = (Worksheet)wb.Worksheets.Add();
			all.Name = "All Tests";
			SetHeaders(all);
			// Create a sheet for failed tests
			var failed = (Worksheet)wb.Worksheets.Add(Type.Missing, all);
			failed.Name = "Failed Tests";
			SetHeaders(failed);
			// Create a sheet for passed tests
			var passed = (Worksheet)wb.Worksheets.Add(failed);
			passed.Name = "Passed Tests";
			SetHeaders(passed);
			// Create a sheet for tests that were skipped
			var notRun = (Worksheet)wb.Worksheets.Add(Type.Missing, failed);
			notRun.Name = "Tests Not Run";
			SetHeaders(notRun);
			// Set initial row counters
			int totalCount = 2, failedCount = 1, passedCount = 1, notRunCount = 1;
			foreach (var area in areas)
			{
				// Add a new worksheet after the previous worksheet or the "not run" worksheet
				ws = (Worksheet)wb.Worksheets.Add(Type.Missing, ws ?? notRun);
				if (string.IsNullOrWhiteSpace(area.Key))
					ws.Name = "Not Specified";
				else
				{
					// Set the name of the worksheet to the area name
					var subStringLenth = area.Key.IndexOf('(') - 1;
					if (subStringLenth < 1)
						ws.Name = area.Key;
					else
						ws.Name = area.Key.Substring(0, subStringLenth);
				}
				// Set the headers for the worksheet
				SetHeaders(ws);

				int count = 2;
				// Loop through each testcase
				foreach (var testcase in area)
				{
					// Add the test case to the "all" worksheet
					AddCaseToSheet(all, testcase, totalCount);
					// Add the test case to this worksheet
					AddCaseToSheet(ws, testcase, count);
					if (!testcase.Passed.HasValue) // Test Not Run, add to worksheet
					{
						notRunCount++;
						AddCaseToSheet(notRun, testcase, notRunCount);
					}
					else if (testcase.Passed.Value) // Test Passed, add to worksheet
					{
						passedCount++;
						AddCaseToSheet(passed, testcase, passedCount);
					}
					else // Test failed, add to worksheet
					{
						failedCount++;
						AddCaseToSheet(failed, testcase, failedCount);
					}
					count++;
					totalCount++;
				}
				// Do not wrap the text
				ws.UsedRange.WrapText = false;
				// Fit column witdth to text size
				ws.Columns.AutoFit();
			}
			// Do not wrap text and autofit text
			all.UsedRange.WrapText = false;
			all.Columns.AutoFit();
			failed.UsedRange.WrapText = false;
			failed.Columns.AutoFit();
			passed.UsedRange.WrapText = false;
			passed.Columns.AutoFit();
			notRun.UsedRange.WrapText = false;
			notRun.Columns.AutoFit();
			// Set the "all" sheet to the active sheet
			all.Activate();
			// Delete "sheet1", save the workbook, and close it
			((Worksheet)wb.Sheets["Sheet1"]).Delete();
			wb.SaveAs(System.IO.Path.Combine(_directory, _fileName));
			wb.Close();
		}
		/// <summary>
		/// Sets the column headers for the worksheet
		/// </summary>
		/// <param name="ws">The worksheet needing column headers to be set</param>
		private static void SetHeaders(Worksheet ws)
		{
			ws.Cells[1, _areaColumn] = "Area";
			ws.Cells[1, _useCaseColumn] = "Use Case";
			ws.Cells[1, _testName] = "Test Name";
			ws.Cells[1, _countColumn] = "# in Set";
			ws.Cells[1, _classColumn] = "Class";
			ws.Cells[1, _resultsColumn] = "Result";
			ws.Cells[1, _messageColumn] = "Message";
		}

		/// <summary>
		/// Adds the test case to the worksheet at the specified row
		/// </summary>
		/// <param name="ws">The worksheet to add the row</param>
		/// <param name="testcase">The test case to add</param>
		/// <param name="rowNumber">The row at which the test case will be added</param>
		private static void AddCaseToSheet(Worksheet ws, TestCase testcase, int rowNumber)
		{
			// Populate the cells for each column
			ws.Cells[rowNumber, _areaColumn] = testcase.Area;
			ws.Cells[rowNumber, _useCaseColumn] = testcase.UseCase;
			ws.Cells[rowNumber, _testName] = testcase.Name;
			ws.Cells[rowNumber, _countColumn] = testcase.Number;
			ws.Cells[rowNumber, _classColumn] = testcase.Class;

			// If not run, set the result column to yellow
			if (!testcase.Passed.HasValue)
			{
				ws.Cells[rowNumber, _resultsColumn] = "Not Run";
				ws.Cells[rowNumber, _resultsColumn].Interior.Color = Microsoft.Office.Interop.Excel.XlRgbColor.rgbLightYellow;
			}
			else if (testcase.Passed.Value) // Passed, set results to green
			{
				ws.Cells[rowNumber, _resultsColumn] = "Passed";
				ws.Cells[rowNumber, _resultsColumn].Interior.Color = Microsoft.Office.Interop.Excel.XlRgbColor.rgbLightGreen;
			}
			else // Failed, set results to red
			{
				ws.Cells[rowNumber, _resultsColumn] = "Failed";
				ws.Cells[rowNumber, _resultsColumn].Interior.Color = Microsoft.Office.Interop.Excel.XlRgbColor.rgbTomato;
			}
			// Populate the error message to the message column
			ws.Cells[rowNumber, _messageColumn] = testcase.ErrorMessage;
		}

		private static ushort _areaColumn = 1;
		private static ushort _useCaseColumn = 2;
		private static ushort _testName = 5;
		private static ushort _countColumn = 4;
		private static ushort _resultsColumn = 3;
		private static ushort _messageColumn = 6;
		private static ushort _classColumn = 7;

		private static string _directory = "";
		private static string _fileName = "Results.xlsx";
		private static Dictionary<string, TestCase> Tests = new Dictionary<string, TestCase>();
		/// <summary>
		/// Processes a Chutzpah ouput file
		/// </summary>
		/// <param name="file">The full name of the file</param>
		private static void ProcessJUnitFile(string file)
		{
			var doc = new XmlDocument();
			doc.Load(file);
			// Find each test case
			var testCases = doc.GetElementsByTagName("testcase");
			// Loop through the test cases
			foreach (XmlElement testCase in testCases)
			{
				// Create the test case as a web interface area
				var parsedCase = new TestCase()
				{
					Id = (Guid.NewGuid()).ToString(),
					Area = string.Format("Web Interfaces (Total: {0})", testCases.Count)
				};
				// Get the full name of the the test case
				var sig = testCase.GetAttribute("name");

				var startIndex = sig.IndexOf(':');
				var endIndex = sig.IndexOf('[', startIndex + 1);
				// Get only the use case part of the full name
				parsedCase.UseCase = sig.Substring(startIndex + 1, endIndex - startIndex - 2).Trim();
				startIndex = endIndex;
				endIndex = sig.IndexOf(']', startIndex + 1);
				// Parse out which number this is in the test case set
				parsedCase.Number = ushort.Parse(sig.Substring(startIndex + 1, endIndex - startIndex - 1));
				// Set the name
				parsedCase.Name = sig.Substring(endIndex + 1).Trim();

				// Find the failure item
				var failureItem = (XmlElement)testCase.GetElementsByTagName("failure").Item(0);
				if (failureItem == null)
				{
					// No failure item, test passed
					parsedCase.Passed = true;
				}
				else
				{
					// Test failed, set the message
					parsedCase.Passed = false;
					parsedCase.ErrorMessage = failureItem.GetAttribute("message");
				}
				// Add the test case to the list
				Tests.Add(parsedCase.Id, parsedCase);
			}
		}

		/// <summary>
		/// Process a visual studio test output file
		/// </summary>
		/// <param name="file">The name of the VS test output file</param>
		private static void ProcessVSFile(string file)
		{
			var doc = new XmlDocument();
			// Read the file
			doc.Load(file);
			// Find the unit tests
			var testCases = doc.GetElementsByTagName("UnitTest");
			// Loop through the unit tests
			foreach (XmlElement testCase in testCases)
			{
				// Create a test case object with the ID and name
				var parsedCase = new TestCase()
				{
					Id = testCase.GetAttribute("id"),
					Name = testCase.GetAttribute("name")
				};
				// Find the categories for the test case
				var categories = testCase.GetElementsByTagName("TestCategoryItem");
				// For each category
				foreach (XmlElement category in categories)
				{
					/* Example
					 * <TestCategoryItem TestCategory="Test Case #: 2" />
						<TestCategoryItem TestCategory="Class: SIR.DAL.DataServer" />
						<TestCategoryItem TestCategory="Area: DataAccessLayer (Total: 20)" />
						<TestCategoryItem TestCategory="Use Case: DataAccessLayer - Connect (Total: 5)" />
					 */
					// Parse out the values for each category and set them on the test case
					var value = category.GetAttribute("TestCategory");
					if (value.StartsWith(_testCaseCategoryNumberFormat))
					{
						parsedCase.Number = ushort.Parse(value.Substring(_testCaseCategoryNumberFormat.Length));
					}
					else if (value.StartsWith(_classCategoryFormat))
					{
						parsedCase.Class = value.Substring(_classCategoryFormat.Length);
					}
					else if (value.StartsWith(_areaCategoryFormat))
					{
						parsedCase.Area = SplitCaps(value.Substring(_areaCategoryFormat.Length));
					}
					else if (value.StartsWith(_ucCategoryFormat))
					{
						parsedCase.UseCase = value.Substring(_ucCategoryFormat.Length);
					}
				}
				// Add the test case to the list of tests
				Tests.Add(parsedCase.Id, parsedCase);
			}

			// Find the results for the unit tests (VS does not group them with the test case)
			var testCaseResults = doc.GetElementsByTagName("UnitTestResult");
			// For each result
			foreach (XmlElement result in testCaseResults)
			{
				// Find the matching test case by test Id
				var tc = Tests[result.GetAttribute("testId")];
				// Determine if the test passed
				tc.Passed = result.GetAttribute("outcome") == "Passed";
				if (!tc.Passed.Value)
				{
					// Did not pass, set the error message
					tc.ErrorMessage = result.GetElementsByTagName("Message").Item(0).InnerText;
				}
			}
		}

		private static string _testCaseCategoryNumberFormat = "Test Case #: ";
		private static string _classCategoryFormat = "Class: ";
		private static string _areaCategoryFormat = "Area: ";
		private static string _ucCategoryFormat = "Use Case: ";

		/// <summary>
		/// Adds spaces between capital letters
		/// </summary>
		/// <param name="value">The string to be split</param>
		/// <returns>A formatted string with spaces inbetween capital letters</returns>
		static string SplitCaps(string value)
		{
			string output = string.Empty;
			// For each character in the string
			for (int ii = 0; ii < value.Length; ii++)
			{
				// If not first, and uppercase, and the previous letter was uppercase, 
				// then add a space and the character to the output, otherwise just add the character
				if (ii > 0 &&
					char.IsUpper(value[ii]) &&
					value[ii - 1] >= 'a' &&
					value[ii - 1] <= 'z')
					output += (" " + value[ii]);
				else
					output += value[ii];
			}
			return output;
		}
	}

	/// <summary>
	/// Represents a test case
	/// </summary>
	class TestCase
	{
		public TestCase()
		{
			var notSpecified = "Not Specified";
			Id = notSpecified;
			Name = notSpecified;
			Area = notSpecified;
			Number = 0;
			Passed = null;
			Class = notSpecified;
			ErrorMessage = notSpecified;
			UseCase = notSpecified;
		}
		/// <summary>
		/// Get or sets the Id for the test case
		/// </summary>
		public string Id { get; set; }
		/// <summary>
		/// Get or sets the name for the test case
		/// </summary>
		public string Name { get; set; }
		/// <summary>
		/// Get or sets the area for the test case
		/// </summary>
		public string Area { get; set; }
		/// <summary>
		/// Get or sets the number in the set for the test case
		/// </summary>
		public ushort Number { get; set; }
		/// <summary>
		/// Get or sets whether or not the test passed
		/// </summary>
		public bool? Passed { get; set; }
		/// <summary>
		/// Get or sets the name of the class for the test case
		/// </summary>
		public string Class { get; set; }
		/// <summary>
		/// Get or sets the error message for the test case
		/// </summary>
		public string ErrorMessage { get; set; }
		/// <summary>
		/// Get or sets the use case for the test case
		/// </summary>
		public string UseCase { get; set; }
	}
}
